using System;
using System.Linq;
using System.Text;
using Generator.Model;
using Generator.Renderer.Internal.Field;

namespace Generator.Renderer.Internal;

internal static class UntypedRecordHandle
{
    public static string Render(GirModel.Record record)
    {
        var typeName = Model.UntypedRecord.GetInternalHandle(record);
        var dataName = Model.UntypedRecord.GetDataName(record);
        var unownedHandleTypeName = Model.UntypedRecord.GetInternalUnownedHandle(record);
        var ownedHandleTypeName = Model.UntypedRecord.GetInternalOwnedHandle(record);
        var arrayHandleType = Model.UntypedRecord.GetInternalArrayHandle(record);
        var arrayUnownedHandleTypeName = Model.UntypedRecord.GetInternalArrayUnownedHandle(record);
        var arrayOwnedHandleTypeName = Model.UntypedRecord.GetInternalArrayOwnedHandle(record);
        var fullyQualifiedType = Model.TypedRecord.GetFullyQualifiedPublicClassName(record);

        return $@"using System;
using GObject;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;

#nullable enable

namespace {Namespace.GetInternalName(record.Namespace)};

// AUTOGENERATED FILE - DO NOT MODIFY

{PlatformSupportAttribute.Render(record as GirModel.PlatformDependent)}
public abstract class {typeName} : SafeHandle
{{    
    public sealed override bool IsInvalid => handle == IntPtr.Zero;

    protected {typeName}(bool ownsHandle) : base(IntPtr.Zero, ownsHandle) {{ }}

    {record.Fields.Select(x => RenderField(record, x)).Join(Environment.NewLine)}

    public bool Equals({typeName}? other)
    {{
        if (ReferenceEquals(null, other))
            return false;

        if (ReferenceEquals(this, other))
            return true;

        return handle.Equals(other.handle);
    }}

    public override bool Equals(object? obj)
    {{
        return ReferenceEquals(this, obj) || obj is {typeName} other && Equals(other);
    }}

    public override int GetHashCode()
    {{
        return handle.GetHashCode();
    }}
}}

public class {unownedHandleTypeName} : {typeName}
{{
    private static {unownedHandleTypeName}? nullHandle;
    public static {unownedHandleTypeName} NullHandle => nullHandle ??= new {unownedHandleTypeName}();

    /// <summary>
    /// Creates a new instance of {unownedHandleTypeName}. Used automatically by PInvoke.
    /// </summary>
    internal {unownedHandleTypeName}() : base(false) {{ }}

    /// <summary>
    /// Creates a new instance of {ownedHandleTypeName}. Assumes that the given pointer is unowned by the runtime.
    /// </summary>
    public {unownedHandleTypeName}(IntPtr ptr) : base(false)
    {{
        SetHandle(ptr);
    }}

    public {ownedHandleTypeName} Copy()
    {{
        var size = Marshal.SizeOf<{dataName}>();
        var ptr = GLib.Functions.Memdup2(handle,(nuint) size);
            
        return new {ownedHandleTypeName}(ptr);
    }}

    protected override bool ReleaseHandle()
    {{
        throw new System.Exception(""UnownedHandle must not be freed"");
    }}
}}

public class {ownedHandleTypeName} : {typeName}
{{
    /// <summary>
    /// Creates a new instance of {ownedHandleTypeName}. Used automatically by PInvoke.
    /// </summary>
    internal {ownedHandleTypeName}() : base(true) {{ }}

    /// <summary>
    /// Creates a new instance of {ownedHandleTypeName}. Assumes that the given pointer is owned by the runtime.
    /// </summary>
    public {ownedHandleTypeName}(IntPtr ptr) : base(true)
    {{
        SetHandle(ptr);
    }}

    /// <summary>
    /// Creates a new owned Handle
    /// </summary>
    public static {ownedHandleTypeName} Create()
    {{
        var size = Marshal.SizeOf<{dataName}>();
        var ptr = GLib.Functions.Malloc((nuint)size);
       
        var str = new {dataName}();
        Marshal.StructureToPtr(str, ptr, false);
            
        return new {ownedHandleTypeName}(ptr);
    }}

    public unsafe void CopyTo(IntPtr ptr)
    {{
        var data = Marshal.PtrToStructure<{dataName}>(handle);
        Marshal.StructureToPtr(data, ptr, false);
    }}

    public {unownedHandleTypeName} UnownedCopy()
    {{
        var size = Marshal.SizeOf<{dataName}>();
        var ptr = GLib.Functions.Memdup2(handle,(nuint) size);
            
        return new {unownedHandleTypeName}(ptr);
    }}

    protected override bool ReleaseHandle()
    {{
        GLib.Functions.Free(handle);
        return true;
    }}
}}

public abstract class {arrayHandleType} : SafeHandle
{{
    public sealed override bool IsInvalid => handle == IntPtr.Zero;

    protected {arrayHandleType}(bool ownsHandle) : base(IntPtr.Zero, ownsHandle) {{ }}
}}

public class {arrayUnownedHandleTypeName} : {arrayHandleType}
{{
    private static {arrayUnownedHandleTypeName}? nullHandle;
    public static {arrayUnownedHandleTypeName} NullHandle => nullHandle ??= new {arrayUnownedHandleTypeName}();
    
    private int length;

    /// <summary>
    /// Creates a new instance of {arrayUnownedHandleTypeName}. Used automatically by PInvoke.
    /// </summary>
    internal {arrayUnownedHandleTypeName}() : base(false) {{ }}
    
    public {arrayUnownedHandleTypeName}(IntPtr ptr, int length) : base(false)
    {{
        this.length = length;
        SetHandle(ptr);
    }}

    public {fullyQualifiedType}[] ToArray(int length)
    {{
        return ToNullableArray(length) ?? throw new InvalidOperationException(""Handle is invalid"");
    }}

    public {fullyQualifiedType}[]? ToNullableArray(int length)
    {{
        if (IsInvalid)
            return null;
        
        var data = new {fullyQualifiedType}[length];
        var currentHandle = handle;
        for(int i = 0; i < length; i++)
        {{
            var ownedHandle = new {Model.UntypedRecord.GetFullyQuallifiedUnownedHandle(record)}(currentHandle).Copy();
            data[i] = new {fullyQualifiedType}(ownedHandle);
            currentHandle += Marshal.SizeOf<{Model.TypedRecord.GetFullyQuallifiedDataName(record)}>();
        }}

        return data;
    }}

    protected override bool ReleaseHandle()
    {{
        throw new System.Exception(""UnownedHandle must not be freed"");
    }}
}}

public class {arrayOwnedHandleTypeName} : {arrayHandleType}
{{
    
    //This has no constructor without parameters as we can't supply a length to an array via pinvoke.
    //The length would need to be set manually and the instance be freed via glib.

    private {arrayOwnedHandleTypeName}(IntPtr ptr) : base(true)
    {{
        SetHandle(ptr);
    }}

    public static {arrayOwnedHandleTypeName} Create({Model.UntypedRecord.GetFullyQualifiedPublicClassName(record)}[] data)
    {{
        var size = Marshal.SizeOf<{Model.UntypedRecord.GetFullyQuallifiedDataName(record)}>();
        var ptr = Marshal.AllocHGlobal(size * data.Length);
        var current = ptr;
        for (int i = 0; i < data.Length; i++)
        {{
            var structure = Marshal.PtrToStructure<{Model.UntypedRecord.GetFullyQuallifiedDataName(record)}>(data[i].Handle.DangerousGetHandle());
            Marshal.StructureToPtr(structure, current, false);
            current += size;
        }}
        
        return new {arrayOwnedHandleTypeName}(ptr);
    }}
    
    protected override bool ReleaseHandle()
    {{
        Marshal.FreeHGlobal(handle);
        return true;
    }}
}}";
    }

    private static string RenderField(GirModel.Record record, GirModel.Field field)
    {
        var renderableField = Fields.GetRenderableField(field);

        if (field is { IsReadable: false, IsWritable: false } || field.IsPrivate)
            return string.Empty;

        var result = new StringBuilder();

        if (field.IsReadable)
            result.AppendLine(RenderFieldGetter(record, field, renderableField));

        if (field.IsWritable)
            result.AppendLine(RenderFieldSetter(record, field, renderableField));

        return result.ToString();

    }

    private static string RenderFieldGetter(GirModel.Record record, GirModel.Field field, RenderableField renderableField)
    {
        var typePrefix = field.AnyTypeOrCallback.IsT1 ? $"{Model.UntypedRecord.GetDataName(record)}." : string.Empty;
        var dataName = Model.UntypedRecord.GetDataName(record);

        return @$"public {typePrefix}{renderableField.NullableTypeName} Get{renderableField.Name}()
{{
    if (IsClosed || IsInvalid)
        throw new InvalidOperationException(""Handle is closed or invalid"");

    return Marshal.PtrToStructure<{dataName}>(handle).{renderableField.Name};
}}";
    }

    private static string RenderFieldSetter(GirModel.Record record, GirModel.Field field, RenderableField renderableField)
    {
        var dataName = Model.UntypedRecord.GetDataName(record);

        return @$"public void Set{renderableField.Name}({renderableField.NullableTypeName} value)
{{
    if (IsClosed || IsInvalid)
        throw new InvalidOperationException(""Handle is closed or invalid"");

    var data = Marshal.PtrToStructure<{dataName}>(handle);
    data.{renderableField.Name} = value;
    Marshal.StructureToPtr(data, handle, false);
}}";
    }


}
